#include "fuse_entrypoints.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <errno.h>
#include <fuse/fuse.h>
#include <dirent.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/xattr.h>

#include <algorithm>
#include <limits>
#include <string>

#include "glog/logging.h"

#include "block_operations.h"
#include "real_fs_operations.h"

using namespace std;

static off_t BLOCK_SIZE = 1024*4;  // Use a 4k block.

static string g_target_dir;
static string g_mount_point;
static void sanitize_path(string* path);

int tffs_getattr(const char *path, struct stat *stbuf) {

    int retval=0;
    string target_path = g_target_dir + path;

    if( 0 != lstat(target_path.c_str(), stbuf) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_readlink(const char *path, char *buf, size_t bufsiz) {
    int retval=0;
    string target_path = g_target_dir + path;
    string link;
    int buf_amt=0;
    int idx =0;

    if( -1 == (buf_amt=readlink(target_path.c_str(), buf, bufsiz)) ) {
      retval = -errno;
      goto done;
    }
    buf[buf_amt]='\0';

    //If we have a symlink with an absolute path into the underlying
    //image we switch it to the tffs file system.  This means that if
    //someone had linked the tffs file system to the underlying file
    //system, we will do the 'wrong' thing here.  We think that's ok.
    //Also: going to be cheaters and use C++ string.
    link = buf;
    idx = link.find(g_target_dir);
    if( idx==0 ) {
      link.erase(0, g_target_dir.length());
      link.insert(0, g_mount_point);
    }
    strncpy(buf, link.c_str(), bufsiz);

 done:
    return retval;
}


int tffs_mknod(const char *path, mode_t mode, dev_t dev) {

    int retval=0;
    string target_path = g_target_dir + path;

    if( 0 != mknod(target_path.c_str(), mode, dev) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_mkdir(const char *path, mode_t mode) {

    int retval=0;
    string target_path = g_target_dir + path;
    
    if( 0 != mkdir(target_path.c_str(), mode) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_unlink(const char *path) {

    int retval=0;
    string target_path = g_target_dir + path;
    
    if( 0 != unlink(target_path.c_str()) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_rmdir(const char *path) {

    int retval=0;
    string target_path = g_target_dir + path;
    
    if( 0 != rmdir(target_path.c_str()) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

//linkname is in tffs, linkdst may be elsewhere and is specified as a global (not tffs relative) path
int tffs_symlink(const char *linkdst, const char *linkname) {

    int retval=0;
    string target_linkname = g_target_dir + linkname;
    string target_linkdst = linkdst;
    int idx=0;
    
    //If we have a symlink with an absolute path into the tffs mount,
    //we actually want that path to be based on the underlying image.
    idx = target_linkdst.find(g_mount_point);
    if( idx==0 ) {
      target_linkdst.erase(0, g_mount_point.length());
      target_linkdst.insert(0, g_target_dir);
    }
    if( 0 != symlink(target_linkdst.c_str(),target_linkname.c_str()) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_chmod(const char *path, mode_t mode) {

    int retval=0;
    string target_path = g_target_dir + path;
    
    if( 0 != chmod(target_path.c_str(), mode) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_chown(const char *path, uid_t owner, gid_t group) {

    int retval=0;
    string target_path = g_target_dir + path;
    
    if( 0 != chown(target_path.c_str(), owner, group) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_truncate(const char *path, off_t length) {

    int retval=0;
    string target_path = g_target_dir + path;
    
    if( 0 != truncate(target_path.c_str(), length) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_rename(const char *old, const char *new_name) {

    int retval=0;
    string target_old = g_target_dir + old;
    string target_new = g_target_dir + new_name;

    if( 0 != rename(target_old.c_str(), target_new.c_str()) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

//TODO: How is the existing target of the link specified?  is it
//relative to the tffs mount as I expect below?  I'm assuming both the
//new link and its target are given in ttfs relative names.
int tffs_link(const char *linkdst, const char *linkname) {

    int retval=0;
    string target_linkname = g_target_dir + linkname;
    string target_linkdst = g_target_dir + linkdst;
    
    if( 0 != link(target_linkdst.c_str(),target_linkname.c_str()) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_utimens(const char *path, const struct timespec ts[2]) {

    int retval=0;
    struct timeval tv[2];

    string target_path = g_target_dir + path;

    tv[0].tv_sec =ts[0].tv_sec;
    tv[1].tv_sec =ts[1].tv_sec;
    tv[0].tv_usec =ts[0].tv_nsec/1000000;
    tv[1].tv_usec =ts[1].tv_nsec/1000000;
    
    if( 0 != utimes(target_path.c_str(),tv) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

//TODO: Can we really just pass ALL flags straight through?
int tffs_open(const char *path, struct fuse_file_info *fi)
{
    int retval = 0;

    string target_path = g_target_dir + path;

    int result = real_open(target_path.c_str(), fi->flags);
    fi->fh = result;
    if (result == -1) {
        retval = -errno;
        goto done;
    }

 done:
    return retval;
}

int tffs_read(const char *path, char *buf, size_t size, off_t offset,
              struct fuse_file_info *fi) {
    if (size < 0 &&
        size > static_cast<size_t>(std::numeric_limits<ssize_t>::max())) {
        return -EINVAL;
    }

    lseek(fi->fh, offset, SEEK_SET);
    return real_read(fi->fh, buf, static_cast<ssize_t>(size));
}

int tffs_write(const char *path, const char *buf, size_t size, off_t offset,
               struct fuse_file_info *fi) {
    if (size < 0 &&
        size > static_cast<size_t>(std::numeric_limits<ssize_t>::max())) {
        return -EINVAL;
    }

    off_t end_offset = offset + size;
    off_t cur_offset = offset;
    ssize_t amt_left = size;
    while (cur_offset < end_offset) {
        off_t new_offset =
            update_block(BLOCK_SIZE, fi->fh, cur_offset, buf, amt_left);
        if (new_offset < 0) {
            LOG(ERROR) << "block update failed during write: "
                       << strerror(-new_offset);
            return new_offset;
        }
        if (new_offset == cur_offset) {
            // Subsequent write attempt yielded no progress. Quit.
            CHECK(false) << "The block_write API should ensure ensure this "
                            "cannot happen.";
            return -ECANCELED;
        }
        ssize_t written = new_offset - cur_offset;
        amt_left -= written;
        buf += written;
        cur_offset = new_offset;
    }

    return size;
}

int tffs_statvfs(const char *path, struct statvfs *buf) {

    int retval=0;
    string target_path = g_target_dir + path;
    
    if( 0 != statvfs(target_path.c_str(), buf) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

/* static int tffs_flush(const char *path, struct fuse_file_info *fi) */
/* { */
/*     return 0; */
/* } */

int tffs_release(const char *path, struct fuse_file_info *fi) {
    if (close(fi->fh) == -1) {
        return -errno;
    }

    return 0;
}

/* static int tffs_fsync(const char *path, struct fuse_file_info *fi) */
/* { */
/*     return 0; */
/* } */

int tffs_setxattr(const char *path, const char *name,
		  const void *value, size_t size, int flags)
{
    int retval=0;
    string target_path = g_target_dir + path;
    
    if( 0 != setxattr(target_path.c_str(), name, value, size, flags) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_getxattr(const char *path, const char *name,
		  void *value, size_t  size) {

    int retval=0;
    string target_path = g_target_dir + path;
    
    if( -1 == (retval=getxattr(target_path.c_str(),name, value, size)) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_listxattr(const char *path, char *list, size_t size)  {
    int retval=0;
    string target_path = g_target_dir + path;

    if( -1 == (retval = listxattr(target_path.c_str(), list, size)) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_removexattr(const char *path, const char *name)  {

    int retval=0;
    string target_path = g_target_dir + path;
    
    if( 0 != removexattr(target_path.c_str(), name) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_opendir(const char *path, struct fuse_file_info *fi) {

    DIR* dp=NULL;
    int retval=0;

    string target_path = g_target_dir + path;

    if( 0 == (dp = opendir(target_path.c_str())) ) {
      retval=-errno;
      goto done;
    }
    fi->fh = (uintptr_t)dp;

 done:
    return retval;
}

int tffs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
                 off_t offset, struct fuse_file_info *fi) {
    
    DIR *dp;
    struct dirent *de;
    int retval=0;

    dp = ((DIR*)(uintptr_t)fi->fh);
	
    if( (de = readdir(dp)) == 0) {
      retval=-errno;
      goto done;
    }

    do {
      struct stat stbuf;
      if( 0 != tffs_getattr(path, &stbuf) ) {
	//opting to struggle on rather than throwing an error
	if (filler(buf, de->d_name, NULL, 0) != 0)
	  return -ENOMEM;
      } else if (filler(buf, de->d_name, &stbuf, 0) != 0) {
	return -ENOMEM;
      }
    } while ((de = readdir(dp)) != NULL);
    
 done:
    return retval;
}

int tffs_releasedir(const char *path, struct fuse_file_info *fi) {

    int retval=0;

    if( 0 != (closedir((DIR*)(uintptr_t)fi->fh)) ) {
      retval=-errno;
      goto done;
    }

 done:
    return retval;
}

/* static int tffs_fsyncdir(const char *path, struct fuse_file_info *fi) */
/* { */
/*     return 0; */
/* } */

int tffs_access(const char *path, int mode)  {

    int retval=0;

    string target_path = g_target_dir + path;
    if( 0 != access(target_path.c_str(), mode) ) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;  
}

int tffs_create(const char *path, mode_t mode, struct fuse_file_info *fi) {
    int retval=0;
    string target_path = g_target_dir + path;

    int result = real_creat(target_path.c_str(), fi->flags, mode);
    fi->fh = result;
    if (result == -1) {
      retval = -errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_ftruncate(const char *path, off_t length, struct fuse_file_info *fi) {
    int retval=0;

    // TODO(awong): We need to remove excess checksums here.
    /*
    if (0 != (retval = truncate_checksums(fi->fh, length))) {
        LOG(ERROR) << "Probleym truncating checksums.";
        return retval;
    }
    */

    if( 0 != (ftruncate(fi->fh, length)) ) {
      retval=-errno;
      goto done;
    }

 done:
    return retval;
}

int tffs_fgetattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi) {

    int retval=0;

    if( 0 != (fstat(fi->fh, stbuf)) ) {
      retval=-errno;
      goto done;
    }

 done:
    return retval;
}


void set_target_dir(const char* path) {
    char result_path[PATH_MAX];
    if (realpath(path, &result_path[0]) == NULL) {
        fprintf(stderr, "Unable to get realpath for target %s: %s", path,
                strerror(errno));
        exit(1);
    }

    g_target_dir = result_path;
    sanitize_path(&g_target_dir);
}

void set_mount_point(const char* path) {
    char result_path[PATH_MAX];
    if (realpath(path, &result_path[0]) == NULL) {
        fprintf(stderr, "Unable to get realpath for target %s: %s", path,
                strerror(errno));
        exit(1);
    }

    g_mount_point = result_path;
    sanitize_path(&g_mount_point);
}

static void sanitize_path(string* path) {
    // Strip off trailing slashes.
    while (!path->empty() &&
           (*path)[path->size()-1] == '/') {
        path->erase(path->size() - 1);
    }

    if (path->empty()) {
        fprintf(stderr, "target directory cannot be empty or all slashes.\n");
        exit(1);
    }
}

const char* get_mount_point() {
    return g_mount_point.c_str();
}

const char* get_target_dir() {
    return g_target_dir.c_str();
}


